声明:首先这篇文章算是翻译+实践的
学习的来源是phrack网站的paper,这个应是上年就看到了,现在才去尝试实践与学习。
其实算是简译再加上自己的实践吧,最后劫持rip起shell由于需要特殊的内核编译才能搞,就没具体实践了,原理上理解了。
看完这篇文章,一句话说明虚拟机逃逸漏洞利用其实就是:
利用qemu代码实现上的漏洞去起一个/bin/sh
什么的(当然执行计算器也是可以的),问题是我们在guest虚拟机里面,我们怎么控制那个/bin/sh呢,那就是通过共享内存交换数据(传递我们的命令到共享内存,最终传递给shell,之后将shell命令的执行结果放入共享内存传递回来guest虚拟机),从而实现在guest虚拟机控制qemu启动的/bin/sh。
简介
无论企业还是个人,都越来越频繁地使用虚拟化技术,从而引出虚拟机逃逸
这个案例讲的是CVE-2015-5165 (信息泄露漏洞) and CVE-2015-7504 (堆溢出漏洞)
KVM/QEMU 总览
KVM(Kernel Virtual Machine)是Linux的一个内核驱动模块,它能够让Linux主机成为一个Hypervisor(虚拟机监控器)。
QEMU(quick emulator)本身并不包含或依赖KVM模块,而是一套由Fabrice Bellard编写的模拟计算机的自由软件。QEMU虚拟机是一个纯软件的实现,可以在没有KVM模块的情况下独立运行,但是性能比较低。QEMU使用了KVM模块的虚拟化功能,为自己的虚拟机提供硬件虚拟化加速以提高虚拟机的性能。
环境搭建
git clone下来后需要回退到漏洞版本
下面编译成x86_64,并且启用调试
1 | $ git clone git://git.qemu-project.org/qemu.git |
当然可能在编译器前需要下载pixman,autoconf
1 | apt install libpixman-1-dev |
之后得装下系统获得一个qcow2的镜像,直接用别人的镜像也行,我下面安装的是ubuntu-16.04.5-server
我们可以新建硬盘,在启动安装,可能需要vnc连接安装一下
1 | qemu-img create -f qcow2 ubuntu.qcow2 20G |
当然我们也可以通过图形化的virt-manager来安装
1 | sudo apt install virt-manager |
镜像可能size是我们设置的硬盘大小20G,可以这样缩小为实际占用空间
1 | sudo qemu-img convert -c -O qcow2 ubuntu16.04.qcow2 ubuntu16.04.qcow2.new |
有了qcow2镜像后,就可以启动了,添加了漏洞相关的两个网卡rtl8139和pcnet,path_to_image自己修改下
1 | ./qemu-system-x86_64 -enable-kvm -m 2048 -display vnc=:89 \ |
当然可以通过添加这个参数-redir tcp:5022::22
映射ssh端口,我们连接5022即可连接qemu里面的ssh
注意vnc的端口是5989(默认端口是5900,5900+89 = 5989,89是上面的参数)
别人直接attach去调试是可以的,但是我们attach上去之后就不能再c了,不知道为何,也没下断点,怎么就有一个-1的断点呢,知道如何解决的告诉我。
所以我只能直接gdb启动qemu了,
调试示例:
1 | gdb --args ./qemu-system-x86_64 -enable-kvm -m 2048 -netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 -redir tcp:5022::22 -netdev user,id=t1, -device pcnet,netdev=t1,id=nic1 -drive file=/ubuntu16.04.qcow2,format=qcow2,if=ide,cache=writeback |
QEMU内存布局
guest虚拟机的物理内存实际上是qemu程序mmap出来的一块private属性的虚拟内存。而且PROT_EXEC这个标志在这个虚拟内存中是不启用的
下面作者的图比较直观
我们去查maps文件的时候,也可以看出来,上面qemu启动命令所设置的2G内存
1 | $ sudo cat /proc/5291/maps |
上面那个为啥是2G,你看看下面的2G的大小,以及下面的地址差
1 | >>> hex(2*1024*1024*1024) |
地址转换
这里面有两层转换
1、从guest 的虚拟机地址 to guest 的物理地址
2、从 guest 的物理地址 to QEMU’s 虚拟地址空间
假如知道了上一小节,这个就不难理解了
在64位系统,虚拟地址是由页面偏移(0到11 bits)和页号组成的。
而且pagemap页面映射文件给了用户空间的进程CAP_SYS_ADMIN权限去找到虚拟地址与物理地址的映射
pagemap页面映射文件包含的虚拟页面是一个64位的值,如下面所示
1 | - Bits 0-54 : physical frame number if present. |
为了将虚拟地址转换成物理地址,使用Nelson Elhage的代码,下面的程序申请了一个buffer,并写入字符串——“Where am I?”,之后打印他的物理地址
1 | #include <stdio.h> |
我们将上面这个代码编译后,放到qemu运行(root权限)
之后我们在主机gdb attach到qemu的pid(root权限)
查看分配给qemu虚拟机对应的内存,我们分配的是2G,所以大小是0x8000000
1 | gdb-peda$ info proc mappings |
可以看到确实可以在这个虚拟地址看到我们字符串
信息泄露利用的实现
下面是CVE-2015-5165,一个 RTL8139 网卡设备模拟器的内存信息泄露漏洞
我们需要获得下面两种地址
1、.text段的基址来构建我们的shellcode
2、guest虚拟机的物理地址,以便得到一些虚拟结构的地址
漏洞代码
REALTEK 网卡支持两种模式:C 和 C+,问题在C+模式的时候,网卡设备模拟器错误地计算了IP数据包数据的长度并最终发送了比实际数据包中的更多数据。
漏洞在hw/net/rtl8139.c
文件的rtl8139_cplus_transmit_one的函数中
1 | /* ip packet header */ |
IP头包含了上面的两个字段, hlen和ip->ip_len
hlen是IP头的长度,这个是固定的20字节,不包括可选字段
ip->ip_len是整个包的总长度,包含ip头部的
而且ip_data_len的类型是uint16_t,即unsigned short int,所以当ip->ip_len小于hlen,计算出的结果是负数,转化为unsigned short int,那就是一个大整数了,最终导致发送的数据超过实际ip data区的数据,从而实现泄露
1 | typedef unsigned short int uint16_t; |
而超过了MTU的长度,会一个chunk一个chunk地传输
下面是部分代码分成一个一个chunk的代码
1 | //通过ip_data_len算出tcp_data_len |
所以我们发送一个恶意的数据包包含特殊的长度(比如ip->ip_len = hlen - 1),ip_data_len是unsigned short int,所以可以泄露0xffff个字节,那就是我们可以泄露约64KB的内存,收到约43个数据包,因为mtu一般1500。
配置网卡
为了发送格式错误的数据包并读取泄漏的数据,我们需要配置第一个Rx和Tx描述符缓冲区,并设置一些
标志位,以便进入易受攻击的代码路径。
下面是RTL8139漏洞相关的寄存器
- TxConfig: Enable/disable Tx flags 比如 TxLoopBack (开启 loopback测试模式
test mode), TxCRC (不添加CRC校验码的 Tx 包), etc. - RxConfig: Enable/disable Rx flags 比如 AcceptBroadcast (接收广播包), AcceptMulticast (接收组播包), etc.
- CpCmd: C+ 命令寄存器用来开启一些函数如下:
CplusRxEnd (enable receive), CplusTxEnd (enable transmit), etc. - TxAddr0: Tx descriptors table的物理地址.
- RxRingAddrLO: Rx descriptors 低32位的物理地址
table. - RxRingAddrHI: Rx descriptors 高32位的地址
table. - TxPoll: 让网卡检查Tx descriptors.
一个Rx/Tx-descriptor就是下面的结构:
buf_lo和buf_hi就是 Tx/Rx 的物理地址的低32和高32位,它们指向发送/接收数据包的缓冲区,必须与页面大小对齐。变量dw0编码了缓冲区的大小还有额外的标记位,比如用来标记缓冲区归网卡还是驱动所有。
1 | struct rtl8139_desc { |
网卡通过in() 和out()进行配置 (from sys/io.h),我们需要有CAP_SYS_RAWIO权限
下面的代码片段配置网卡,并设置单个Tx描述符
struct rtl8139_desc {
uint32_t dw0;
uint32_t dw1;
uint32_t buf_lo;
uint32_t buf_hi;
};
网卡的设置通过 in() out() 原语来配置 (from
sys/io.h). 而且我们需要 CAP_SYS_RAWIO 权限才能配置. 下面的代码片段配置了网卡还有初始化了一个Tx descriptor。
1 | #define RTL8139_PORT 0xc000 |
漏洞利用
漏洞利用代码设置了网卡的寄存器还有Tx 和 Rx buffer descriptors,之后发一个异常格式的数据包到网卡的MAC地址,这样我们就可以通过访问Rx缓冲区来读取泄露的数据了
泄露的数据中有几个函数指针,而他们是同一个QEMU内部结构的成员
1 | typedef struct ObjectProperty |
应该说是这几个
1 | ObjectPropertyAccessor *get; |
qemu遵循对象模型来管理设备,内存区域等,qemu启动的时候,会创建多个对象并为其分配属性。
比如下面的函数会给一个内存区域对象真机一个may-overlap的属性,这个属性有一个getter方法去获取这个属性的布尔值
1 | object_property_add_bool(OBJECT(mr), "may-overlap", |
RTL8139网卡设备仿真器 在堆上用一个64KB的内存来重新组装数据包。而这个64K的buffer有很大机会把free掉了的object properties的内存占位了
在漏洞利用中,我们在泄漏的内存中搜索已知的对象属性。更确切地说,我们正在寻找80字节的内存块(块的大小为已经free掉的ObjectProperty结构,80加上堆头16,就是96,即0x60),其中至少有一个函数指针(get, set, resolve or release),即使开了ASLR,我们仍然可以获得.text部分的基地址。实际上,他们的页面偏移是固定的(12个最低有效位不是随机的),我们可以通过一些简单的计算获得qemu一些有用的函数的地址,我们也可以得到libc的一些地址,比如mprotect() 和 system() 的地址
我们还注意到地址PHY_MEM + 0x78泄漏了几次,其中PHY_MEM是给guest虚拟机分配的物理内存的起始地址
exp就是搜索泄露的内存数据并尝试解析,.text段的虚拟地址,以及物理内存的基址
这个可能需要一行一行的读代码才能好理解
我们可以通过build-exploit.sh,生成的各种函数的相对偏移,之后替换掉qemu.h,再编译cve-2015-5165.c即可
实验结果:
堆溢出的利用
这个小节是讨论CVE-2015-7504,同时提供控制rip的exp
漏洞代码
AMD PCNET网卡模拟器在本地回环测试模式下收到大的数据包时存在堆溢出漏洞,PCNET模拟器保留了4Kb(4096 bytes)的buffer来存储数据包。如果Tx descriptor buffer上的ADDFCS标志位是开启的,网卡会在收到的数据包后面添加一个CRC校验码,这个是在hw/net/pcnet.c里面的pcnet_receive()函数实现的。
收到的数据包小于(4096 - 4)个bytes是没有问题的,但是数据包刚好是4096个bytes,我们就可以溢出这个buffer 4个字节的大小了
1 | uint8_t *src = s->buffer; |
在上面的代码中s指向PCNET结构体,我们看看这个结构体,buffer后面就是irq,我们可以覆盖irq变量的值
1 | struct PCNetState_st { |
变量irq是一个指IRQState结构体的指针,而这个指针里面第二是一个handler
1 | typedef struct IRQState *qemu_irq; |
而这个handler会被PCNET网卡模拟器调用多次。比如,在pcnet_receive()的末尾,调用了pcnet_update_irq(),这个函数里面调用了qemu_set_irq(),在qemu_set_irq中就调用了irq中的handler
1 | void qemu_set_irq(qemu_irq irq, int level) |
所以要利用这个漏洞,我们需要:
伪造一个假的IRQState结构体,比如里面包含指向system函数的handler
获得这个假的IRQState结构体的精确地址。有了之前的信息泄露,我们可以算出它在qemu进程的内存地址(这是在guest虚拟机的物理内存基址再加上一个偏移)
伪造一个4K的恶意数据包(即4096字节).
修改数据包,使得计算出来的CRC刚好指向我们构造的假的IRQState结构体
最后就发送这个数据包即可
但PCNET网卡收到数据包,它会通过pcnet_receive函数处理执行以下操作:
- 复制收到的数据包到buffer变量
- 计算CRC并追加到buffer后面,那么就会溢出buffer4个bytes,覆盖了irq变量
- 调用 pcnet_update_irq(),里面再调用 qemu_set_irq() ,在里面就调用irq变量里面的handler,那么我我们的handler就会执行了
请注意,irq是我们伪造的,所以我们可以控制irq->handler的前两个参数 (irq->opaque and irq->n), 感谢一个小技巧,我们也可以控制第三个参数(level),这对于调用mprotect函数来说是必须的。
还需要注意的是我们是用4字节覆盖一个8字节的指针,覆盖的是低4字节,在我们的测试环境中我们可以成功控制rip寄存器。然而这会在没有在编译时设置CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE标志的内核上会出问题。
设置网卡
在进一步研究之前,我们需要设置PCNET网卡需要的flags,Tx 和 Rx 描述缓冲区,还有环形缓冲区,以便能够发送和接收数据包。
AMD PCNET网卡可以以16位模式或者32位模式进行访问。这个取决于DWI0的值(这个值储存在网卡中),下面我们深入PCNET网卡在16位模式下的主要寄存器,因为16位模式网卡重置后的默认模式。
通过访问reset寄存器可以将卡重置为默认状态。
PCNET网卡有两种内部寄存器:CSR (Control and Status Register 控制和状态寄存器) and BCR (Bus Control Registers 总线控制寄存器)。两个寄存器都需要在RAP(Register Address Port) 寄存器设置我们要访问的寄存器索引才能访问。比如我们想初始化并重启网卡,我们需要将CSR0的bit0和bit1设置为1,这个我们可以通过写入0到RAP寄存器去选择CSR0,之后设置CSR为0x3,(即二进制的0b11),如下面代码所示:
1 | outw(0x0, PCNET_PORT + RAP); |
网卡的配置可以通过初始化一个下面的结构体之后传递这个结构体的物理地址给网卡(需要通过CSR1和CSR2寄存器完成)
1 | struct pcnet_config { |
逆向CRC
就像前面所说的,我们需要填充数据包,使得计算出来的CRC能够指向我们伪造的IRQState结构体。
幸运的是,CRC是可逆的,只需要打一个4字节的补丁即可让他设置成我们想要设置的任何值
下面的源码reverse-crc.c,打了一个4字节的补丁,使得CRC计算出来是0xdeadbeef
1 | ---[ reverse-crc.c ]--- |
漏洞利用
漏洞利用首先将网卡设置为默认模式,之后配置Tx和Rx描述缓冲区,最后初始化网卡,重启网卡使设置生效
之后就是发一个触发堆溢出漏洞的数据包,如下所示,qemu_set_irq调用了一个损坏的irq->handler地址——0x7f66deadbeef。qemu就会崩溃,因为那是一个非法的地址。
1 | (gdb) shell ps -e | grep qemu |
这是我实践的截图,由于要取irq的handler的值,由于irq是0x5555deadbf1f,这个地址是不可读的,所以崩溃了
将上面两个漏洞结合起来实现完整利用
这一小节,结合前两个漏洞进行虚拟机逃逸,并使用qemu的权限在主机上执行代码
首先,我们使用CVE-2015-5165来重构qemu的内存布局(其实就是信息泄露),更确切地说,是获得下面的一些地址以绕过ASLR保护:
- guest虚拟机的物理内存基址,在漏洞利用中,我们需要在虚拟机里面申请分配一些内存,获得这个内存在qemu虚拟地址空间的精确地址
- .text 段的基址,这可以让我们获得qemu_set_irq()函数的地址
- .plt段的基址,这可以让我们知道一些函数的地址,比如fork()和execv()函数,他们可以用来构建我们的shellcode。我们mprotect() 函数来改变guest虚拟机的物理地址的权限——记住,分配给guest虚拟机的“物理地址”是不可执行的(即qemu的mmap出来的地址)。
控制RIP
在上面我们是可以控制rip寄存器的。假如我们想不然qemu崩溃,我们得溢出PCNET网卡的buffer使得irq结构指向一个我们伪造的IRQState,那就会call我们想调用的函数了。
首先,我们可能会尝试构建一个假的IRQState结构去调用system函数,然而这会失败,因为一些qemu映射的内存fork之后不能使用这段内存,更确切的说是mmap的物理内存有MADV_DONTFORK标记。(具体可以看这里http://man7.org/linux/man-pages/man2/madvise.2.html , 搜索MADV_DONTFORK关键字)
1 | qemu_madvise(new_block->host, new_block->max_length, QEMU_MADV_DONTFORK); |
调用execv()也是没用的,因为这样我们会是去对guest虚拟机的控制权
还有一种想法是我们可以构造一种shellcode——将几个假的IRQState连起来去调用多个函数,因为PCNET网卡模拟器或调用好几次qemu_set_irq()。然而我们发现这样子更方便更可靠——我们先开启shellcode所在内存页的PROT_EXEC标志,之后再执行shellcode。
我们现在的想法是构造两个假的IRQState结构。第一个结构用于调用mprotect(),第二个就用于调用shellcode——这个shellcode首先撤销MADV_DONTFORK标志,之后执行一个在guest虚拟机和主机之间可交互的shell。
如前所述,但qemu_set_irq()被调用,它有两个参数——irq (指向 IRQstate 的结构体) 和 level (IRQ level),之后如下所示调用handler:
1 | void qemu_set_irq(qemu_irq irq, int level) |
如上,我们只能控制前两个参数,那么有三个参数的mprotect(),我们如何调用呢?
为了解决这个问题,我们使qemu_set_irq()调用自身,其中参数如下:(其实就是将handler设置为qemu_set_irq函数的地址,我们即可控制level,这操作牛逼)
- irq: 指向假的IRQState,其中handler指针指向mprotect()函数
- level: mprotect的flag设置为: PROT_READ | PROT_WRITE | PROT_EXEC
这是通过设置两个假的IRQState来实现的,代码片段如下:
1 | struct IRQState { |
那么现在,就是溢出后,qemu_set_irq()调用了一个fake handler,而这个handler就是qemu_set_irq()自身,这可以将level参数设置为7,而这个是mprotect说需要的,那么之后就是调用mprotect函数了。
内存现在是可执行的了,我们可以通过将第一个IRQState的handler指向我们的shellcode地址,之后就可以将控制权交给我们的交互式shell。
payload.fake_irq[0].handler = shellcode_addr;
payload.fake_irq[0].arg_1 = shellcode_data;
交互式shell
我们可以写一个基础的shellcode——在shell绑定到netcat的某个端口上,之后通过其他计算机连接这个shell。这是一个满意的解决方案,但是我们最好能够规避防火墙。我们可以利用guest虚拟机和主机之间的共享内存来构建一个bindshell。
利用qemu的漏洞有一点微妙,我们在guest虚拟机写的代码,在qemu进程的内存中是可用的。所以我们不用注入shellcode,我们可以共享代码,使它在guest虚拟机运行,之后攻击host主机。
下面总结了在host主机和guest虚拟机之间的共享内存和进程,线程。
我们创建两个共享的环形buffer(in和out)并提供通过自旋锁访问这些共享内存区域读/写的原语。在host主机上,我们运行一段shellcode——运行一个 /bin/sh 的shell,并且复制它的 stdin 和 stdout 文件描述符。我们创建两个线程,第一个从共享内存读取命令并通过管道传递给shell,第二个线程读取shell的输出(从第二个管道读),之后将他们写到共享内存。
guest虚拟机也有两个线程,第一个线程将用户输入的命令写到共享内存上,第二个线程从共享内存中读取到的输出stdout
请注意,在我们的exp中,我们有第三个线程(还有一个专用的共享内存)来处理stderr的输出
下面其实看图更加清晰:
VM-Escape Exploit
在这一小节,我们概述完整exp(vm-escape.c)的主要结构和函数。
注入的payload由下面的结构体定义:
1 | struct payload { |
上面的fake_irq是一对假的IRQState结构体,目的是调用mprotect()去改变我们paylaod所在页面的保护为可读可写可执行。
结构体shared_data是用于将参数传递给主shellcode的。
1 | struct shared_data { |
got结构体充当全局偏移表(Global Offset Table,即GOT表),它包含了shellcode所需的主要函数的地址,这些地址是我们是通过信息泄露获得的。
1 | struct GOT { |
主shellcode是由下面的结构体定义的:
1 | /* main code to run after %rip control */ |
上面的shellcode()函数首先检查一下 shared_data->done,避免shellcode()函数执行多次(因为qemu_set_irq会被qemu代码调用多次,而qemu_set_irq又会调用shellcode函数)
shellcode()函数之后调用madvise()函数这是撤销shared_data->addr pointing的MADV_DONTFORK标志,这可以确保fork之后内存映射还是可用的。
shellcode()函数接下来是创建了一个子进程——就是启动一个shell(“/bin/sh”)。父进程则启动了3个线程,到共享内存区域将shell命令从guest虚拟机传递到host主机,之后将这些命令执行结果的输出给回guest虚拟机。父进程与子进程的通信则通过管道来通信。
如下所示,共享内存区域包含一个环形缓冲区,可以通过sm_read() 和 sm_write()原语进行访问。
1 | struct shared_ring_buf { |
上面的两个原语是由下面的线程函数使用的。第一个是从共享内存区域读取数据,然后写到一个文件描述符中。第二个的话是从文件描述符中读取,然后写到共享内存区域
These primitives are used by the following threads function. The first one
reads data from a shared memory area and writes it to a file descriptor.
The second one reads data from a file descriptor and writes it to a shared
memory area.
1 | void *pipe_r2fd(void *_brwpipe) |
注意代码里面的这些函数是共享于host主机和guest虚拟机。这些线程也在guest虚拟机实例化,读取用户输入的命令,之后复制他们专用的共享内存区域(in这个变量的共享内存区域)。还有将命令的输出写到共享内存区域(out和err这两个变量共享内存)
1 | void session(struct shared_io *shared_io) |
上面我们讨论说明了共享内存,在guest和host里面运行的进程/线程。
这个exploit作者使用了gcc 4.9.2进行编译。未来适应特定的qemu,作者提供了一个shell脚本(build-exploit.sh)来输出一个qemu.h头文件,其实获得的是各种我们需要的函数什么的偏移。
使用方法如下
1 | $ ./build-exploit <path-to-qemu-binary> > qemu.h |
编译直接-o会出错,得加个-pthread
1 | gcc vm-escape.c -pthread -o vm-escape |
Running the full exploit (vm-escape.c) will result in the following output:
1 | $ ./vm-escape |
由于我的实验环境的内核编译的时候应该没有加入CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE这个选项,所以irq指针是在qemu的堆里面,而不在qemu给guest所mmap出来的内存里面,而我们又只能溢出4个字节,所以只能失败
局限性
请注意,目前的漏洞利用仍然是不可靠的。在测试环境中(Debian 7 running a 3.16 kernel on x_86_64 arch),10次中大概有1次是失败的。在大多数失败的情况中,exp不能重构qemu的内存布局,因为泄露的数据是不可用的(其实就是泄露的数据不靠谱,导致计算出的其他函数的地址是错误的)
同样exploit也不适用于内核编译没有加入CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE标志的。这种情况下,qemu二进制程序(默认加入-fPIE进行编译)会映射到单独的内存地址空间中,如下所示:
1 | 55e5e3fdd000-55e5e4594000 r-xp 00000000 fe:01 6940407 [qemu-system-x86_64] |
因此,我们的4字节溢出不足以覆盖irq指针(因为地址在0x55xxxxxxxxxx处的堆中)指向我们假的IRQState结构,而IRQState结构在0x7fxxxxxxxxxx
总结
在本文中,我们展示了QEMU的网络设备模拟器的两个漏洞。 这些漏洞利用的结合使得突破VM并在主机上执行代码成为可能。
在这项工作中,我们可能会crash我们的测试VM1000次。 调试不成功的漏洞利用会很繁琐,特别是,使用复杂的shellcode函数去多进程,而一个进程又启动多个线程。因此,我们希望已经提供了足够的技术细节以及可以重复用于进一步利用QEMU的通用技术。
感谢
原作者的感谢的话我就不贴出来了
实验源码
下面文章的Source Code部分有uuencode编码的内容,将begin…end之间拷贝到一个文件——命名为666.txt(你喜欢什么名字都可以)
执行命令
uudecode 666.txt
就得到vm_escape.tar.gz,再解压就可以了